package at.ac.tuwien.dse.health.dashboard;

import at.ac.tuwien.dse.health.dashboard.dto.Notification;
import at.ac.tuwien.dse.health.dashboard.dto.SurgerySlotDTO;
import at.ac.tuwien.dse.health.entity.*;
import at.ac.tuwien.dse.health.event.bus.EventBus;
import at.ac.tuwien.dse.health.reservation.ReservationCancellation;
import at.ac.tuwien.dse.health.reservation.ReservationRequest;
import at.ac.tuwien.dse.health.security.*;
import at.ac.tuwien.dse.health.shared.FilterSettings;
import com.google.common.base.*;
import com.google.common.collect.Collections2;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;

import static com.google.common.base.Preconditions.*;
import static com.google.common.base.Strings.*;
import static com.google.common.collect.Lists.newArrayList;
import static org.apache.commons.lang3.ObjectUtils.*;

/**
 * @author Bernhard Nickel
 * @author Gregor Schauer
 * @author Dominik Strasser
 */
@Service
@Transactional
public class HealthService {
	private static final Logger LOGGER = LogManager.getFormatterLogger(HealthService.class);

	static final ThreadLocal<DateFormat> dayFormat = new ThreadLocal<DateFormat>() {
		@Override
		protected DateFormat initialValue() {
			return new SimpleDateFormat("dd.MM.yyyy");
		}
	};
	static final ThreadLocal<DateFormat> timeFormat = new ThreadLocal<DateFormat>() {
		@Override
		protected DateFormat initialValue() {
			return new SimpleDateFormat("HH:mm");
		}
	};

	@PersistenceContext
	EntityManager entityManager;
	@Inject
	EventBus eventBus;

	@Secured(Hospital.class)
	public SurgerySlot deleteSlot(long slotId) {
		SurgerySlot slot = entityManager.find(SurgerySlot.class, slotId);
		if (slot == null) {
			LOGGER.debug("Unable to delete non-existent slot %d", slotId);
			return null;
		}
		Hospital hospital = (Hospital) SecurityContext.getRole();
		if (!slot.getHospital().getId().equals(hospital.getId())) {
			LOGGER.debug("Unable to delete slot '%d'. The slot is provided by '%s', however, the delete request came from '%s'", slotId, slot.getHospital().getName(), hospital.getName());
			return null;
		}
		if (!slot.isFree()) {
			LOGGER.debug("Removing reserved slot '%d' ('%s'). Patient: '%d', Doctor: '%d'", slot.getId(), slot.getHospital().getName(), slot.getPatient().getId(), slot.getDoctor().getId());
			eventBus.fire(new ReservationCancellation(slot.getPatient().getId(), slot.getDoctor().getId(), slot.getDepartment().getId(), slot.getHospital().getId(), slot.getStartDate(), slot.getEndDate()));
		} else {
			LOGGER.debug("Removing free slot '%d' ('%s')", slotId, slot.getHospital().getName());
		}
		entityManager.remove(slot);
		return slot;
	}

	@Secured(Hospital.class)
	public SurgerySlot createSlot(Date startDate, Date endDate) {
		if (!isValidDateRange(startDate, endDate)) {
			return null;
		}
		Hospital hospital = (Hospital) SecurityContext.getRole();
		SurgerySlot slot = new SurgerySlot();
		slot.setStartDate(startDate);
		slot.setEndDate(endDate);
		slot.setHospital(hospital);
		entityManager.persist(slot);
		LOGGER.debug("'%s' created new slot", hospital.getName());
		return slot;
	}

	boolean isValidDateRange(Date startDate, Date endDate) {
		return startDate != null && endDate != null && endDate.getTime() > startDate.getTime()
				&& startDate.getTime() >= System.currentTimeMillis();
	}

	@SuppressWarnings("unchecked")
	@Secured
	public List<SurgerySlot> listSlots(final FilterSettings settings) {
		Object role = SecurityContext.getRole();
		String queryName;
		if (role instanceof Patient) {
			queryName = SurgerySlot.GET_SLOTS_FOR_PATIENT;
		} else if (role instanceof Doctor) {
			queryName = SurgerySlot.GET_SLOTS_FOR_DOCTOR;
		} else if (role instanceof Hospital) {
			queryName = SurgerySlot.GET_SLOTS_FOR_HOSPITAL;
		} else {
			return newArrayList();
		}

		List<SurgerySlot> slots = entityManager.createNamedQuery(queryName, SurgerySlot.class)
				.setParameter("filterObject", role).getResultList();

		return settings == null ? slots : newArrayList(Collections2.filter(slots, new Predicate<SurgerySlot>() {
			@Override
			public boolean apply(SurgerySlot input) {
				Date date = firstNonNull(settings.getDate(), new Date());
				return input.getStartDate().after(date)
						&& (settings.getFree() == null || settings.getFree() == input.isFree())
						&& (isNullOrEmpty(settings.getHospital()) || settings.getHospital().equals(input.getHospital().getName()))
						&& (isNullOrEmpty(settings.getSurgeryType()) || settings.getSurgeryType().equals(input.getDepartment().getName()));
			}
		}));
	}

	@Secured(Doctor.class)
	public Patient getPatient(Integer socialSecurityNumber) {
		if (socialSecurityNumber == null || socialSecurityNumber < 1000 || socialSecurityNumber > 9999) {
			return null;
		}
		try {
			return entityManager.createNamedQuery(Patient.VALIDATE_SOCIAL_SECURITY_NUMBER, Patient.class)
					.setParameter("socialSecurityNumber", socialSecurityNumber).getSingleResult();
		} catch (NoResultException e) {
			return null;
		}
	}

	@Transactional(readOnly = true)
	public List<String> getDepartmentNames() {
		return entityManager.createNamedQuery(Department.GET_DEPARTMENT_NAMES, String.class).getResultList();
	}

	@Transactional(readOnly = true)
	public List<String> getHospitalNames() {
		return entityManager.createNamedQuery(Hospital.GET_HOSPITAL_NAMES, String.class).getResultList();
	}

	@Secured(Doctor.class)
	public ReservationRequest requestReservation(int socialSecurityNumber, int radius, String type) {
		if (type != null && !type.trim().isEmpty() && socialSecurityNumber > 0 && radius > 0) {
			try {
				if (!getDepartmentNames().contains(type)) {
					return null;
				}
				Patient p = entityManager.createNamedQuery(Patient.VALIDATE_SOCIAL_SECURITY_NUMBER, Patient.class)
						.setParameter("socialSecurityNumber", socialSecurityNumber).getSingleResult();

				ReservationRequest request = new ReservationRequest(((Doctor) SecurityContext.getRole()).getId(), p.getId(), type, radius);
				eventBus.fire(request);
				return request;
			} catch (NoResultException e) {
				return null;
			}
		}
		return null;
	}

	@Secured(Doctor.class)
	public ReservationCancellation cancelReservation(long slotId) {
		SurgerySlot slot = entityManager.find(SurgerySlot.class, slotId);
		checkArgument(slot != null, "Surgery slot with ID '%s' does not exist", slotId);
		checkArgument(!slot.isFree(), "Surgery slot with ID '%s' is not reserved", slotId);
		checkArgument(SecurityContext.getRole().equals(slot.getDoctor()), "Permission denied");

		ReservationCancellation cancellation = new ReservationCancellation(
				slot.getPatient().getId(), slot.getDoctor().getId(), slot.getDepartment().getId(),
				slot.getHospital().getId(), slot.getStartDate(), slot.getEndDate());

		slot.setDepartment(null);
		slot.setDoctor(null);
		slot.setPatient(null);

		eventBus.fire(cancellation);

		return cancellation;
	}

	@Secured
	public List<Message> getUnreadMessages() {
		Object user = SecurityContext.getRole();
		String queryName;
		if (user instanceof Patient) {
			queryName = Message.BY_PATIENT;
		} else if (user instanceof Doctor) {
			queryName = Message.BY_DOCTOR;
		} else if (user instanceof Hospital) {
			queryName = Message.BY_HOSPITAL;
		} else {
			return newArrayList();
		}

		Date now = new Date();
		List<Message> messages = entityManager.createNamedQuery(queryName, Message.class).setParameter("user", user).getResultList();
		for (Message message : messages) {
			message.setReceiveDate(now);
		}
		return messages;
	}

	static class SurgerySlotMappingFunction implements Function<SurgerySlot, SurgerySlotDTO> {
		@Override
		public SurgerySlotDTO apply(SurgerySlot slot) {
			if (slot == null || slot.getStartDate() == null || slot.getEndDate() == null) {
				throw new IllegalArgumentException("Invalid surgery slot passed");
			}
			SurgerySlotDTO dto = new SurgerySlotDTO(slot.getId(),
					dayFormat.get().format(slot.getStartDate()),
					timeFormat.get().format(slot.getStartDate()),
					timeFormat.get().format(slot.getEndDate()),
					ObjectUtils.toString(slot.getDoctor(), null),
					ObjectUtils.toString(slot.getDepartment(), null),
					slot.getHospital().getName(),
					slot.isFree());
			dto.setPatient(ObjectUtils.toString(slot.getPatient(), null));
			return dto;
		}
	}

	static class MessageMappingFunction implements Function<Message, Notification> {
		@Override
		public Notification apply(Message input) {
			MessageType type = Enum.valueOf(MessageType.class, input.getType().toUpperCase());
			return new Notification(type, input.getTitle(), input.getMessage());
		}
	}
}
